package info.hexin.redis.distributedlock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import redis.clients.jedis.exceptions.JedisConnectionException;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

/**
 * Created by Administrator on 2016/1/29.
 */
public class RedisDistributedLockServiceImpl implements RedisDistributedLockService {
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisDistributedLockServiceImpl.class);

    private static final int DEFAULT_SINGLE_EXPIRE_TIME = 3;

    private static final int DEFAULT_BATCH_EXPIRE_TIME = 6;

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 获取锁  如果锁可用   立即返回true，  否则返回false
     *
     * @param billIdentify
     * @return
     * @author http://blog.csdn.net/java2000_wl
     */
    public boolean tryLock(String billIdentify) {
        return tryLock(billIdentify, 0L, null);
    }

    /**
     * 锁在给定的等待时间内空闲，则获取锁成功 返回true， 否则返回false
     *
     * @param billIdentify
     * @param timeout
     * @param unit
     * @return
     * @author http://blog.csdn.net/java2000_wl
     */
    public boolean tryLock(String billIdentify, long timeout, TimeUnit unit) {
        String key = billIdentify;
        try {
            long nano = System.nanoTime();
            do {
                LOGGER.debug("try lock key: " + key);
                Boolean result = setNx(key, key);
                if (result) {
                    redisTemplate.expire(key, DEFAULT_SINGLE_EXPIRE_TIME, TimeUnit.SECONDS);
                    LOGGER.debug("get lock, key: " + key + " , expire in " + DEFAULT_SINGLE_EXPIRE_TIME + " seconds.");
                    return Boolean.TRUE;
                } else { // 存在锁
                    if (LOGGER.isDebugEnabled()) {
                        String desc = (String) getRedisValueByKey(key);
                        LOGGER.debug("key: " + key + " locked by another business：" + desc);
                    }
                }
                if (timeout == 0) {
                    break;
                }
                Thread.sleep(300);
            } while ((System.nanoTime() - nano) < unit.toNanos(timeout));
            return Boolean.FALSE;
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
        return Boolean.FALSE;
    }

    /**
     * 如果锁空闲立即返回   获取失败 一直等待
     *
     * @param billIdentify
     * @author http://blog.csdn.net/java2000_wl
     */
    public void lock(String billIdentify) {
        String key = (String) billIdentify;
        try {

            do {
                LOGGER.debug("lock key: " + key);
                boolean resutl = setNx(key, key);
                if (resutl) {
                    redisTemplate.expire(key, DEFAULT_BATCH_EXPIRE_TIME, TimeUnit.SECONDS);
                    LOGGER.debug("get lock, key: " + key + " , expire in " + DEFAULT_SINGLE_EXPIRE_TIME + " seconds.");
                    return;
                } else {
                    if (LOGGER.isDebugEnabled()) {
                        String desc = (String) getRedisValueByKey(key);
                        LOGGER.debug("key: " + key + " locked by another business：" + desc);
                    }
                }
                Thread.sleep(300);
            } while (true);
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);

        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * 释放锁
     *
     * @param billIdentify
     * @author http://blog.csdn.net/java2000_wl
     */
    public void unLock(String billIdentify) {
        List<String> list = new ArrayList<String>();
        list.add(billIdentify);
        unLock(list);
    }

    /**
     * 批量获取锁  如果全部获取   立即返回true, 部分获取失败 返回false
     *
     * @param billIdentifyList
     * @return
     * @author http://blog.csdn.net/java2000_wl
     * @date 2013-7-22 下午10:27:44
     */
    public boolean tryLock(List<String> billIdentifyList) {
        return tryLock(billIdentifyList, 0L, null);
    }

    /**
     * 锁在给定的等待时间内空闲，则获取锁成功 返回true， 否则返回false
     *
     * @param billIdentifyList
     * @param timeout
     * @param unit
     * @return
     * @author http://blog.csdn.net/java2000_wl
     */
    public boolean tryLock(final List<String> billIdentifyList, long timeout, TimeUnit unit) {

        try {
            final List<String> needLocking = new CopyOnWriteArrayList<String>();
            List<String> locked = new CopyOnWriteArrayList<String>();
            long nano = System.nanoTime();
            do {
                // 构建pipeline，批量提交
                List<Object> results = redisTemplate.executePipelined(new RedisCallback() {
                    public Object doInRedis(RedisConnection connection) throws DataAccessException {
                        StringRedisConnection stringRedisConn = (StringRedisConnection) connection;
                        for (String identify : billIdentifyList) {
                            String key = identify;
                            needLocking.add(key);
                            stringRedisConn.setNX(key, key);
                        }
                        return true;//必须返回null
                    }
                });

                LOGGER.debug("try lock keys: " + needLocking);
                // 提交redis执行计数
                for (int i = 0; i < results.size(); ++i) {
                    Long result = (Long) results.get(i);
                    String key = needLocking.get(i);
                    if (result == 1) {    // setnx成功，获得锁
                        redisTemplate.expire(key, DEFAULT_BATCH_EXPIRE_TIME, TimeUnit.SECONDS);
                        locked.add(key);
                    }
                }
                needLocking.removeAll(locked);    // 已锁定资源去除

                if (CollectionUtils.isEmpty(needLocking)) {
                    return true;
                } else {
                    // 部分资源未能锁住
                    LOGGER.debug("keys: " + needLocking + " locked by another business：");
                }

                if (timeout == 0) {
                    break;
                }
                Thread.sleep(500);
            } while ((System.nanoTime() - nano) < unit.toNanos(timeout));

            // 得不到锁，释放锁定的部分对象，并返回失败
            if (!CollectionUtils.isEmpty(locked)) {
                redisTemplate.delete(locked.toArray(new String[0]));
            }
            return false;
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
        return true;
    }

    /**
     * 批量释放锁
     *
     * @param billIdentifyList
     * @author http://blog.csdn.net/java2000_wl
     */
    public void unLock(List<String> billIdentifyList) {
        List<String> keys = new CopyOnWriteArrayList<String>();
        for (String identify : billIdentifyList) {
            String key = identify;
            keys.add(key);
        }
        try {
            redisTemplate.delete(keys);
            LOGGER.debug("release lock, keys :" + keys);
        } catch (JedisConnectionException je) {
            LOGGER.error(je.getMessage(), je);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    private boolean setNx(String key, Object value) {
        return redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    private Object getRedisValueByKey(String key) {
        return redisTemplate.opsForValue().get(key);
    }

}
